<?php

namespace League\CLImate;

use Psr\Log\AbstractLogger;
use Psr\Log\InvalidArgumentException;
use Psr\Log\LogLevel;
use function str_replace;
use function strpos;

/**
 * A PSR-3 compatible logger that uses CLImate for output.
 */
class Logger extends AbstractLogger
{
    /**
     * @var array conversion of the level strings to their numeric representations
     */
    private $levels = [
        LogLevel::EMERGENCY => 1,
        LogLevel::ALERT => 2,
        LogLevel::CRITICAL => 3,
        LogLevel::ERROR => 4,
        LogLevel::WARNING => 5,
        LogLevel::NOTICE => 6,
        LogLevel::INFO => 7,
        LogLevel::DEBUG => 8,
    ];

    /**
     * @var int ignore logging attempts at a level less than this
     */
    private $level;

    /**
     * @var CLImate the underlying climate instance we are using for output
     */
    private $climate;

    /**
     * Create a new Logger instance.
     *
     * @param string  $level   One of the LogLevel constants
     * @param CLImate $climate An existing CLImate instance to use for output
     */
    public function __construct($level = LogLevel::INFO, ?CLImate $climate = null)
    {
        $this->level = $this->convertLevel($level);

        if (null === $climate) {
            $climate = new CLImate();
        }
        $this->climate = $climate;

        // Define some default styles to use for the output
        $commands = [
            'emergency' => ['white', 'bold', 'background_red'],
            'alert' => ['white', 'background_yellow'],
            'critical' => ['red', 'bold'],
            'error' => ['red'],
            'warning' => 'yellow',
            'notice' => 'light_cyan',
            'info' => 'green',
            'debug' => 'dark_gray',
        ];

        // If any of the required styles are not defined then define them now
        foreach ($commands as $command => $style) {
            if (!$this->climate->style->get($command)) {
                $this->climate->style->addCommand($command, $style);
            }
        }
    }

    /**
     * Get a new instance logging at a different level
     *
     * @param string $level One of the LogLevel constants
     *
     * @return Logger
     */
    public function withLogLevel($level)
    {
        $logger = clone $this;
        $logger->level = $this->convertLevel($level);

        return $logger;
    }

    /**
     * Log messages to a CLImate instance.
     *
     * @param string        $level   One of the LogLevel constants
     * @param object|string $message If an object is passed it must implement __toString()
     * @param array         $context Placeholders to be substituted in the message
     */
    public function log($level, $message, array $context = [])
    {
        if ($this->convertLevel($level) > $this->level) {
            return;
        }

        // Handle objects implementing __toString
        $message = (string)$message;

        // Handle any placeholders in the $context array
        foreach ($context as $key => $val) {
            $placeholder = '{' . $key . '}';

            // If this context key is used as a placeholder, then replace it, and remove it from the $context array
            if (false !== strpos($message, $placeholder)) {
                $val = (string)$val;
                $message = str_replace($placeholder, $val, $message);
                unset($context[$key]);
            }
        }

        // Send the message to the climate instance
        $this->climate->{$level}($message);

        // Append any context information not used as placeholders
        $this->outputRecursiveContext($level, $context, 1);
    }

    /**
     * Get a numeric log level for the passed parameter.
     *
     * @param string $level One of the LogLevel constants
     *
     * @return int
     */
    private function convertLevel($level)
    {
        // If this is one of the defined string log levels then return it's numeric value
        if (!\array_key_exists($level, $this->levels)) {
            throw new InvalidArgumentException("Unknown log level: {$level}");
        }

        return $this->levels[$level];
    }

    /**
     * Handle recursive arrays in the logging context.
     *
     * @param string $level   One of the LogLevel constants
     * @param array  $context The array of context to output
     * @param int    $indent  The current level of indentation to be used
     */
    private function outputRecursiveContext($level, array $context, $indent)
    {
        foreach ($context as $key => $val) {
            $this->climate->tab($indent);

            $this->climate->{$level}()->inline("{$key}: ");

            if (\is_array($val)) {
                $this->climate->{$level}('[');
                $this->outputRecursiveContext($level, $val, $indent + 1);
                $this->climate->tab($indent)->{$level}(']');
            } else {
                $this->climate->{$level}((string)$val);
            }
        }
    }
}
